package joc.internal.transformer;

import java.lang.annotation.Annotation;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.ParameterizedType;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javassist.ByteArrayClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.LoaderClassPath;
import javassist.NotFoundException;
import joc.JOCCondition;
import joc.JOCEnabled;
import joc.JOCExternalContract;
import joc.Pure;
import joc.contract.ContainsStringContract;
import joc.contract.DenyContract;
import joc.contract.Equals;
import joc.contract.EqualsContract;
import joc.contract.GreaterThanLongContract;
import joc.contract.LessThanLongContract;
import joc.contract.NotNullContract;
import joc.contract.ProfileContract;
import joc.customize.Contract;
import joc.internal.ConfigCache;
import joc.internal.transformer.helper.AnnotationInjector;
import joc.internal.transformer.helper.BackdoorAnnotationLoader;
import joc.internal.transformer.helper.ImplicitContract;
import joc.internal.util.SortedProperties;

import org.apache.log4j.Logger;

public class Transformer implements ClassFileTransformer {

	private Logger logger = Logger.getLogger(Transformer.class);
	private ClassPool pool = ClassPool.getDefault();

	@SuppressWarnings("unchecked")
	private final static List<Class<? extends Contract<?>>> contracts = new ArrayList<Class<? extends Contract<?>>>(
			Arrays.asList(ProfileContract.class, DenyContract.class, GreaterThanLongContract.class,
					NotNullContract.class, ContainsStringContract.class, LessThanLongContract.class,
					EqualsContract.class));
	private final static Map<Class<? extends Annotation>, Class<? extends Contract<?>>> contractMap = new HashMap<Class<? extends Annotation>, Class<? extends Contract<?>>>();
	private final static Map<CtClass, CtClass> externalContractMap = new HashMap<CtClass, CtClass>();

	private final List<ClassTransformer> transformers = new ArrayList<ClassTransformer>(Arrays.asList(
			new CustomPostHookBehaviorTransformer(this), new CustomPreHookBehaviorTransformer(this),
			new CustomInvariantClassTransformer(this), new CustomPreConditionBehaviorTransformer(this),
			new PostConditionMethodTransformer(this), new CustomPostConditionBehaviorTransformer(this),
			new ParameterPreConditionBehaviorTransformer(this), new FieldInvariantTransformer(this),
			new GenericBehaviorTransformer(this), new GenericClassTransformer(this), new PureMethodTransformer(this)));

	private final List<ClassTransformer> conditionTransformers = new ArrayList<ClassTransformer>(
			Arrays.asList(new ConditionBehaviorTransformer(this)));

	private final List<String> enabledPackages = new ArrayList<String>();
	private final List<ImplicitContract> implicitContracts = Arrays.asList(new ImplicitContract("implicit-equals",
			Equals.class, Object.class, "equals"), new ImplicitContract("implicit-equals-pure", Pure.class,
			Object.class, "equals"), new ImplicitContract("implicit-hashcode-pure", Pure.class, Object.class,
			"hashCode"));
	private final List<ImplicitContract> enabledImplicitContracts = new ArrayList<ImplicitContract>();

	private static Transformer lastTransformer;

	public static Transformer getLastTransformer() {
		return lastTransformer;
	}

	static {
		for (Class<? extends Contract<?>> contract : contracts) {
			addContractToMap(contract);
		}
	}

	public ClassPool getPool() {
		return pool;
	}

	private static void addContractToMap(Class<? extends Contract<?>> contract) {
		contractMap.put(getAnnotationFromContractClass(contract), contract);
	}

	@SuppressWarnings("unchecked")
	private static Class<? extends Annotation> getAnnotationFromContractClass(Class<?> contractClass) {
		return (Class<? extends Annotation>) ((ParameterizedType) contractClass.getGenericSuperclass())
				.getActualTypeArguments()[0];
	}

	protected static void addContract(Class<? extends Contract<?>> contract) {
		contracts.add(contract);
		addContractToMap(contract);
	}

	protected static void removeContract(Class<? extends Contract<?>> contract) {
		contracts.remove(contract);
		contractMap.remove(getAnnotationFromContractClass(contract));
	}

	protected static List<Class<? extends Contract<?>>> getContracts() {
		return contracts;
	}

	public static Map<Class<? extends Annotation>, Class<? extends Contract<?>>> getContractMap() {
		return contractMap;
	}

	public Transformer(Instrumentation inst) {
		lastTransformer = this;
		loadCustomContracts();
		loadExternalContracts();
		loadEnabledPackages();
		loadGeneralConfig();
	}

	public List<ImplicitContract> getEnabledImplicitContracts() {
		return enabledImplicitContracts;
	}

	public List<ImplicitContract> getImplicitContracts() {
		return implicitContracts;
	}

	private void loadGeneralConfig() {
		List<String> configValues = getConfigValues(ConfigCache.PREFIX_GENERAL);
		for (ImplicitContract implicitContract : implicitContracts) {
			if (configValues.contains(implicitContract.getConfigValue())) {
				enabledImplicitContracts.add(implicitContract);
			}
		}
	}

	private void loadEnabledPackages() {
		enabledPackages.addAll(getConfigValues(ConfigCache.PREFIX_ENABLE));
		for (String enabledPackage : enabledPackages) {
			System.out.println(enabledPackage);
		}
	}

	private List<String> getConfigValues(String prefix) {
		List<String> values = new ArrayList<String>();
		SortedProperties properties = ConfigCache.getConfig();
		for (String key : properties.stringPropertyNames(prefix)) {
			if (properties.getProperty(key).equals(ConfigCache.ON)) {
				values.add(key.substring(prefix.length()));
			}
		}
		return values;
	}

	private void loadExternalContracts() {
		for (String contractString : getConfigValues(ConfigCache.PREFIX_EXTERNAL)) {
			CtClass contract = getExternalContract(contractString);
			if (contract == null) {
				return;
			}
			CtClass classUnderContract = getClassUnderContract(contractString, contract);
			if (classUnderContract == null) {
				return;
			}
			externalContractMap.put(classUnderContract, contract);
			logger.info(classUnderContract.getName() + " is using external contract " + contract.getName());
		}
	}

	private CtClass getClassUnderContract(String contractString, CtClass contract) {
		String classUnderContractString = new BackdoorAnnotationLoader(contract).getClassValue(
				JOCExternalContract.class, "value");
		if (classUnderContractString == null) {
			logger.error("External Contract " + contractString + " is missing Annotation @JOCExternalContract.");
			return null;
		}
		CtClass classUnderContract;
		try {
			classUnderContract = pool.get(classUnderContractString);
		} catch (NotFoundException e) {
			logger.error("Could not find external contract " + contractString + " for class "
					+ classUnderContractString + ".");
			return null;
		}
		return classUnderContract;
	}

	private CtClass getExternalContract(String contractString) {
		CtClass contract;
		try {
			contract = pool.get(contractString);
		} catch (NotFoundException e) {
			logger.error("Could not find external contract " + contractString + ".");
			return null;
		}
		return contract;
	}

	private void loadCustomContracts() {
		for (String contractClass : getConfigValues(ConfigCache.PREFIX_CONTRACT)) {
			try {
				@SuppressWarnings("unchecked")
				Class<? extends Contract<?>> contract = (Class<? extends Contract<?>>) Class.forName(contractClass);
				logger.info("adding contract " + contract.getName());
				addContract(contract);
			} catch (ClassNotFoundException e) {
				logger.error("Could not find custom contract " + contractClass + " - it will be ignored by JOC.");
			}
		}
	}

	@Override
	public byte[] transform(ClassLoader loader, String classNameWithSlashes, Class<?> classBeingRedefined,
			ProtectionDomain protectionDomain, byte[] classfileBuffer) {
		String className = classNameWithSlashes.replace('/', '.');
		logger.debug("transformation started for class " + className);
		try {
			if (loader != null) {
				pool.insertClassPath(new LoaderClassPath(loader));
			}
			if (classfileBuffer != null) {
				pool.insertClassPath(new ByteArrayClassPath(className, classfileBuffer));
			}

			CtClass targetClass = pool.get(className);
			if (targetClass.isInterface()) {
				logger.debug("transformation aborted, as class is an interface");
				return null;
			}
			if (externalContractMap.keySet().contains(targetClass)) {
				CtClass externalContract = externalContractMap.get(targetClass);
				new AnnotationInjector(targetClass, pool).injectExternalContract(externalContract);
				logger.info("transforming class " + className + " with external contract " + externalContract.getName());
			}
			for (String enabledPackage : enabledPackages) {
				if (targetClass.getName().startsWith(enabledPackage + ".")) {
					new AnnotationInjector(targetClass, pool).addJOCEnabledAnnotation();
					logger.info("package " + enabledPackage + " is enabled, implicitly adding "
							+ JOCEnabled.class.getName() + " to " + className);
					break;
				}
			}
			boolean transformed = false;
			if (targetClass.hasAnnotation(JOCEnabled.class)) {
				logger.info("transforming class " + className);
				transformClass(targetClass, transformers);
				transformed = true;
			}
			if (targetClass.hasAnnotation(JOCCondition.class)) {
				logger.info("transforming condition " + className);
				transformClass(targetClass, conditionTransformers);
				transformed = true;
			}
			if (!transformed) {
				logger.debug("transformation aborted, as class does not annotate JOCEnabled or JOCCondition");
				return null;
			} else {
				return targetClass.toBytecode();
			}
		} catch (NotFoundException e) {
			logger.warn("transformation failed for class '" + className + "': could not load class", e);
		} catch (JOCTransformationException e) {
			logger.warn(e.getMessage(), e);
		} catch (Throwable e) {
			e.printStackTrace();
			logger.warn("transformation failed for class '" + className + "': unknown error", e);
		}
		return null;
	}

	private void transformClass(CtClass targetClass, List<ClassTransformer> transformers) throws Exception {
		new AnnotationInjector(targetClass, pool).injectImplicitContracts(enabledImplicitContracts);
		for (ClassTransformer transformer : transformers) {
			transformer.transform(targetClass);
		}
	}

	public Logger getLogger() {
		return logger;
	}

	public void setLogger(Logger logger) {
		this.logger = logger;
	}

	protected void setPool(ClassPool pool) {
		this.pool = pool;
	}

}
